---
title: Operations and processes
summary: End-to-end processing workflows behind the web service operation surface.
status: draft
owner: Not determined from source
last_reviewed: 2026-09-22
---

# Operations and processes

`clsADUserServices` implements 19 processes, one per published WebMethod, with no dispatcher or handler class between the SOAP entry point and the process logic — every operation calls its own process directly. Eight processes are gated by the network-origin allow-list documented in [Authentication and authorization](authentication-authorization.md); the other eleven are directory reads and oracles with no application-layer gate. All processing is against Active Directory; there is no database and no process-wide shared state (see [Architecture](architecture.md)).

This page is the process catalog and hub. Each process's detailed, step-by-step logic lives in its own page under `operations/`.

## Process catalog

| ID | Process | Entry operation | Trigger | Owning component | Rules |
| --- | --- | --- | --- | --- | --- |
| [OP-01](operations/hello-world.md) | Hello World greeting | [`HelloWorld`](contracts/clsaduserservices.md#helloworld) | Any SOAP call | `clsADUserServices.HelloWorld` | — |
| [OP-02](operations/add-user.md) | Create AD account | [`AddUser`](contracts/clsaduserservices.md#adduser) | Caller requests account creation | `clsADUserServices.AddUser` | [BR-01](business-rules.md#br-01-mutating-and-password-operations-require-an-approved-caller), [BR-05](business-rules.md#br-05-a-denied-request-is-reported-as-a-normal-response), [BR-07](business-rules.md#br-07-the-directory-domain-is-selected-by-the-caller)–[BR-13](business-rules.md#br-13-an-optional-attribute-is-written-only-when-supplied), [BR-34](business-rules.md#br-34-a-directory-lookup-failure-is-reported-as-not-found) |
| [OP-03](operations/unlock-account.md) | Unlock AD account | [`UnlockAccount`](contracts/clsaduserservices.md#unlockaccount) | Caller requests unlock | `clsADUserServices.UnlockAccount` | [BR-01](business-rules.md#br-01-mutating-and-password-operations-require-an-approved-caller), [BR-14](business-rules.md#br-14-an-unlock-request-for-an-unknown-user-is-reported-as-such), [BR-15](business-rules.md#br-15-an-account-is-unlocked-only-when-locked-and-the-result-is-re-verified) |
| [OP-04](operations/add-user-to-group.md) | Grant group membership | [`AddUserToGroup`](contracts/clsaduserservices.md#addusertogroup) | Caller requests membership grant | `clsADUserServices.AddUserToGroup` | [BR-01](business-rules.md#br-01-mutating-and-password-operations-require-an-approved-caller), [BR-16](business-rules.md#br-16-a-group-membership-addition-requires-both-user-and-group-to-exist), [BR-17](business-rules.md#br-17-adding-an-already-a-member-user-is-a-distinct-outcome), [BR-33](business-rules.md#br-33-a-group-is-resolved-by-samaccountname%2C-taking-the-first-match) |
| [OP-05](operations/delete-user.md) | Delete AD account | [`DeleteUser`](contracts/clsaduserservices.md#deleteuser) | Caller requests deletion | `clsADUserServices.DeleteUser` | [BR-01](business-rules.md#br-01-mutating-and-password-operations-require-an-approved-caller), [BR-18](business-rules.md#br-18-deleting-a-non-existent-user-is-a-successful-no-op) |
| [OP-06](operations/delete-user-from-group.md) | Revoke group membership | [`DeleteUserFromGroup`](contracts/clsaduserservices.md#deleteuserfromgroup) | Caller requests membership revocation | `clsADUserServices.DeleteUserFromGroup` | [BR-01](business-rules.md#br-01-mutating-and-password-operations-require-an-approved-caller), [BR-19](business-rules.md#br-19-group-removal-drops-to-the-underlying-directoryentry), [BR-20](business-rules.md#br-20-a-failed-group-removal-is-reported-as-success) |
| [OP-07](operations/does-user-exist.md) | Test account existence | [`DoesUserExist`](contracts/clsaduserservices.md#doesuserexist) | Caller queries existence | `clsADUserServices.DoesUserExist` | [BR-06](business-rules.md#br-06-read-only-directory-queries-are-not-access-controlled), [BR-21](business-rules.md#br-21-a-user-exists-when-a-principal-resolves) |
| [OP-08](operations/does-user-have-resource.md) | Test transitive resource membership | [`DoesUserHaveResource`](contracts/clsaduserservices.md#doesuserhaveresource) | Caller queries resource membership | `clsADUserServices.DoesUserHaveResource` | [BR-06](business-rules.md#br-06-read-only-directory-queries-are-not-access-controlled), [BR-22](business-rules.md#br-22-a-user-has-a-resource-by-exact-transitive-group-name-match) |
| [OP-09](operations/change-user-account.md) | Update account attributes | [`ChangeUserAccount`](contracts/clsaduserservices.md#changeuseraccount) | Caller requests an attribute change | `clsADUserServices.ChangeUserAccount` | [BR-01](business-rules.md#br-01-mutating-and-password-operations-require-an-approved-caller), [BR-13](business-rules.md#br-13-an-optional-attribute-is-written-only-when-supplied), [BR-23](business-rules.md#br-23-an-account-change-updates-only-non-empty-fields), [BR-24](business-rules.md#br-24-directory-attribute-changes-commit-only-when-modified-is-raised) |
| [OP-10](operations/change-user-password.md) | Reset password (sentinel on failure) | [`ChangeUserPassword`](contracts/clsaduserservices.md#changeuserpassword) | Caller requests a password reset | `clsADUserServices.ChangeUserPassword` | [BR-01](business-rules.md#br-01-mutating-and-password-operations-require-an-approved-caller), [BR-25](business-rules.md#br-25-a-password-change-is-applied-via-setpassword-and-committed), [BR-26](business-rules.md#br-26-two-password-change-variants-differ-only-in-error-strategy) |
| [OP-11](operations/change-user-password-or-get-exception.md) | Reset password (faulting variant) | [`ChangeUserPasswordOrGetException`](contracts/clsaduserservices.md#changeuserpasswordorgetexception) | Caller requests a password reset and wants failures as faults | `clsADUserServices.ChangeUserPasswordOrGetException` | [BR-01](business-rules.md#br-01-mutating-and-password-operations-require-an-approved-caller), [BR-25](business-rules.md#br-25-a-password-change-is-applied-via-setpassword-and-committed), [BR-26](business-rules.md#br-26-two-password-change-variants-differ-only-in-error-strategy) |
| [OP-12](operations/get-user-email.md) | Read email attribute | [`GetUserEmail`](contracts/clsaduserservices.md#getuseremail) | Caller queries email | `clsADUserServices.GetUserEmail` | [BR-06](business-rules.md#br-06-read-only-directory-queries-are-not-access-controlled), [BR-27](business-rules.md#br-27-a-directory-attribute-query-returns-the-value-or-userdoesnotexist) |
| [OP-13](operations/get-user-groups.md) | Read all group names | [`GetUserGroups`](contracts/clsaduserservices.md#getusergroups) | Caller queries group membership | `clsADUserServices.GetUserGroups` | [BR-06](business-rules.md#br-06-read-only-directory-queries-are-not-access-controlled), [BR-28](business-rules.md#br-28-the-group-list-includes-nested-membership) |
| [OP-14](operations/get-user-resources.md) | Read filtered resource group names | [`GetUserResources`](contracts/clsaduserservices.md#getuserresources) | Caller queries resource entitlements | `clsADUserServices.GetUserResources` | [BR-06](business-rules.md#br-06-read-only-directory-queries-are-not-access-controlled), [BR-29](business-rules.md#br-29-a-resource-is-a-resource-named-group-filtered-by-caller-supplied-environment) |
| [OP-15](operations/get-user-fax.md) | Read fax attribute | [`GetUserFAX`](contracts/clsaduserservices.md#getuserfax) | Caller queries fax number | `clsADUserServices.GetUserFAX` | [BR-06](business-rules.md#br-06-read-only-directory-queries-are-not-access-controlled), [BR-27](business-rules.md#br-27-a-directory-attribute-query-returns-the-value-or-userdoesnotexist) |
| [OP-16](operations/get-user-last-logon.md) | Read last-logon timestamp | [`GetUserLastLogon`](contracts/clsaduserservices.md#getuserlastlogon) | Caller queries last logon | `clsADUserServices.GetUserLastLogon` | [BR-06](business-rules.md#br-06-read-only-directory-queries-are-not-access-controlled), [BR-30](business-rules.md#br-30-last-logon-is-derived-from-the-filetime-attribute) |
| [OP-17](operations/get-user-printer.md) | Read printer attribute (internal domain only) | [`GetUserPrinter`](contracts/clsaduserservices.md#getuserprinter) | Caller queries printer assignment | `clsADUserServices.GetUserPrinter` | [BR-06](business-rules.md#br-06-read-only-directory-queries-are-not-access-controlled), [BR-31](business-rules.md#br-31-the-printer-attribute-is-read-from-the-internal-domain-only) |
| [OP-18](operations/is-user-in-group.md) | Test direct group membership | [`IsUserInGroup`](contracts/clsaduserservices.md#isuseringroup) | Caller queries direct membership | `clsADUserServices.IsUserInGroup` | [BR-06](business-rules.md#br-06-read-only-directory-queries-are-not-access-controlled), [BR-32](business-rules.md#br-32-group-membership-for-isuseringroup-is-direct-only), [BR-33](business-rules.md#br-33-a-group-is-resolved-by-samaccountname%2C-taking-the-first-match) |
| [OP-19](operations/check-access.md) | Evaluate allow-list decision | [`CheckAccess`](contracts/clsaduserservices.md#checkaccess) | Any SOAP call to `CheckAccess`, or internally from each of the eight gated operations | `clsADUserServices.CheckAccess` | [BR-02](business-rules.md#br-02-a-local-request-is-always-approved)–[BR-04](business-rules.md#br-04-the-caller's-source-address-prefers-the-forwarded-for-header), [BR-06](business-rules.md#br-06-read-only-directory-queries-are-not-access-controlled) |

## Cross-process flow diagram

The diagram below traces how a caller's operation request is routed through the OP-19 `CheckAccess` gate to either a mutating process or a read process, both of which operate against Active Directory.

```mermaid
flowchart TD
    Caller["SOAP caller"] --> Gate{"Gated operation?<br>OP-02 OP-03 OP-04 OP-05<br>OP-06 OP-09 OP-10 OP-11"}
    Gate -->|"Yes"| CheckAccess["OP-19 CheckAccess<br>local request or IP allow-list"]
    CheckAccess -->|"Denied"| Denied["AccessDenied sentinel<br>Response = Failure"]
    CheckAccess -->|"Approved"| Mutate["Execute mutating process<br>against external.saif.com"]
    Gate -->|"No — read operation<br>OP-01 OP-07 OP-08 OP-12-18"| Read["Execute read process<br>against caller-selected domain"]
    Mutate --> AD["Active Directory"]
    Read --> AD
    AD --> Response["ADServiceResponse / Boolean / ArrayList<br>returned to caller"]
    Denied --> Response
```

## Failure and edge-case behavior
- **Access denied** (OP-02–OP-06, OP-09–OP-11): caller receives `ADServiceResponse{Response=Failure, Message="Access Denied"}` as a normal SOAP response, never a fault — see [Faults and error pipeline](faults.md).
- **Allow-list setting missing or environment service unreachable** (any gated operation, via OP-19): the unhandled exception propagates as a framework SOAP fault out of the calling operation instead of returning `AccessDenied` — see [Tech debt](tech-debt.md) TD-12.
- **User not found** (most operations): reported as `UserDoesNotExist`, `False`, or an empty list depending on the operation's return shape — the same signal a genuine directory failure produces via [BR-34](business-rules.md#br-34-a-directory-lookup-failure-is-reported-as-not-found), so "not found" and "directory unreachable" are indistinguishable to the caller.
- **Group removal or resource lookup failure** (OP-06, OP-08): `DeleteUserFromGroup`'s catch block deliberately swallows any exception and reports `Success` (TD-20); `DoesUserHaveResource` has no `Try`/`Catch` at all and a directory failure surfaces as a framework SOAP fault.
- **No transactional rollback across two-stage writes** (OP-02 `AddUser`, OP-09 `ChangeUserAccount`): neither process opens a database transaction — there is no database — and each of `userPrin.Save()`/principal-level `Save()` and `dirEntry.CommitChanges()`/directory-level `CommitChanges()` commits independently, with no enclosing transaction and no rollback trigger; a failure between the two stages leaves the object partially applied in Active Directory, a tech-debt finding rather than a documented recovery mechanism ([TD-18](tech-debt.md#high)).
- **Partial write on exception** (OP-02 `AddUser`): a caught `Save()` failure sets `Response = Failure` but execution continues into the directory-attribute block, operating on an unsaved principal (TD-18).
- **Silent no-op on commit** (OP-09 `ChangeUserAccount`): a request that changes only `company` and/or `info` never sets the `modified` flag, so `CommitChanges()` is skipped while the caller still receives `Success` (TD-19).
- **Empty-catch swallow** (OP-13 `GetUserGroups`, OP-14 `GetUserResources`): any directory failure yields an empty `ArrayList`, indistinguishable from "user has no groups/resources" (TD-21).

## Related documentation
- [Service index](index.md)
- [Overview](overview.md)
- [Architecture](architecture.md)
- [Activation and hosting](activation-hosting.md)
- [Operations](operations.md)
- [Authentication and authorization](authentication-authorization.md)
- [Business rules](business-rules.md)
- [Configuration](configuration.md)
- [Dependencies](dependencies.md)
- [Faults and error pipeline](faults.md)
- [Operational runbook](operational-runbook.md)
- [Tech debt](tech-debt.md)

> **Note:** Statements marked with * were inferred from code structure, naming conventions, or
> contextual evidence where direct confirmation was not available in source artifacts. Statements
> marked with ** could not be confirmed; treat as unknown until verified.

## Sources
- `src\ADUserServices\clsADUserServices.asmx.vb`
- `.saifdocgen\state\runs\20260922-101326-aduserservices\analysis\aduserservices-analysis.md`
